/**
 * 
 */
package org.codesketch.birch.command;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.codesketch.birch.common.BirchRequest;

import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceStateChange;
import com.amazonaws.services.ec2.model.InstanceStateName;
import com.amazonaws.services.ec2.model.Reservation;

/**
 * Helper class exposes common and useful method for commands.
 * @author Quirino Brizi
 *
 */
public class CommandUtils {
	
	private static final long WAIT_FOR = 5000;
	private CommandUtils() {}
	
	/**
	 * Wait that all the instance part of the given {@link Reservation} are at the specified state, 
	 * the wait will be halted if a timeout of 5 minutes expires.
	 * @param crowd the crowd client
	 * @param reservation the AWS reservation object
	 * @param desiredState the desired state
	 * @throws IllegalArgumentException if the timeout value is less than zero.
	 */
	public static Boolean waitForTransition(
			final Birch crowd, final Collection<String> identifiers, final InstanceStateName desiredState) {
		
		return waitForTransition(crowd, identifiers, desiredState, null);
	}
	/**
	 * Wait that all the instance part of the given {@link Reservation} are at the specified state, 
	 * the wait will be halted if at least one instance goes on the invalid state or a timeout of 
	 * 5 minutes expires. 
	 * @param crowd the crowd client
	 * @param reservation the AWS reservation object
	 * @param desiredState the desired state
	 * @param invalidState the invalid state
	 * @return true if all the instances are at the desired state, false in case at least one instance 
	 * state is the invalid one.
	 * @throws IllegalArgumentException if the timeout value is less than zero.
	 */
	public static Boolean waitForTransition(
			final Birch crowd, final Collection<String> identifiers, 
			final InstanceStateName desiredState, final InstanceStateName invalidState){
		
		return waitForTransition(crowd, identifiers, desiredState, invalidState, 300L);
	}
	/**
	 * Wait that all the instance part of the given {@link Reservation} are at the specified state, 
	 * the wait will be halted if at least one instance goes on the invalid state
	 * @param crowd the crowd client. 
	 * @param reservation the AWS reservation object
	 * @param desiredState the desired state
	 * @param invalidState the invalid state
	 * @param timeout the check timeout in seconds as a positive long value. A value of 0 means no timeout.
	 * @return true if all the instances are at the desired state, false in case at least one instance
	 * state is the invalid one.
	 * @throws IllegalArgumentException if the timeout value is less than zero.
	 */
	public static Boolean waitForTransition(
			final Birch crowd, final Collection<String> identifiers, final InstanceStateName desiredState, 
			final InstanceStateName invalidState, final Long timeout){
		
		boolean tsValid = true;
		boolean tsComplete = false;
		
		final long start = System.currentTimeMillis() * 1000;
		final int total = identifiers.size();
		final AmazonEC2 ec2 = crowd.client();
		/*
		 * Create AWS request straight (doesn't make sense to use the mapping).
		 */
		final DescribeInstancesRequest req = new DescribeInstancesRequest();
		req.setInstanceIds(identifiers);
		while(!tsComplete) {
			/*
			 * Check if timeout has been expired
			 */
			if(timedout(start, timeout)) {
				tsValid = false;
				break;
			}
			/*
			 * Issue the describe request to AWS.
			 */
			final DescribeInstancesResult res = ec2.describeInstances(req);
			final List<Reservation> reservations = res.getReservations();
			/*
			 * Assumes the sum of instances on all the reservations is equal to 
			 * the number of identifiers.
			 */
			int atState = 0;
			for (Reservation reser : reservations) {
				for (Instance instance : reser.getInstances()) {
					final InstanceStateName state = InstanceStateName.fromValue(instance.getState().getName());
					if(invalidState != null && state.equals(invalidState)) {
						tsValid = false;
						tsComplete = true;
						break;
					}
					if(state.equals(desiredState)) {
						atState += 1;
					}
				}
			}
			/*
			 * is the number of tx at state equal to the number of instances? if so 
			 * the transition is complete.
			 */
			if(atState == total) {
				tsComplete = true;
			}
			if(!tsComplete) {
				try {
					Thread.sleep(WAIT_FOR);
				} catch (InterruptedException e) {
					/*
					 *  can be a shutdown request from the top application
					 *  return irrespectively of the instances status so the 
					 *  main application can get the right decisions.
					 */
					tsValid = false;
					break;
				}
			}
		}
		
		return tsValid;
	}
	/**
	 * Maps {@link BirchRequest} to an AWS aware request.
	 * @param crowdRequest the crowd request
	 * @param awsRequest the AWS request
	 * @throws SecurityException if the AWS request method s not accessible
	 * @throws NoSuchMethodException if the setter method doesn't exists on the AWS request. 
	 * @throws IllegalAccessException if the argument passed to the AWS setter method are not of 
	 * the declared type
	 * @throws InvocationTargetException if the AWS request setter throws an exception
	 */
	public static void map(
			final BirchRequest crowdRequest, final AmazonWebServiceRequest awsRequest
		) throws SecurityException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {
		
		final Class<? extends AmazonWebServiceRequest> clazz = awsRequest.getClass();
		for(String key : crowdRequest.keySet()) {
			
			final Object obj = crowdRequest.get(key);
			if(null==obj) {
				continue;
			}
			Field field = null;
			try {
				field = clazz.getDeclaredField(key);
				field.setAccessible(true);
				field.set(awsRequest, obj);
			} catch (IllegalArgumentException e) {
				final Class<?> arg = obj.getClass();
				final Class<?> type = field.getType();
				// set a set into a list field.
				if(arg.isAssignableFrom(HashSet.class) && type.isAssignableFrom(List.class)) {
					List<?> list = new ArrayList<Object>( (Set<?>) obj );
					field.set(awsRequest, list);
				}
			}
		}
	}
	/**
	 * Return a list containing all the instances identifiers on the given reservation.
	 * @param reservation the AWS reservation
	 * @return a list of instance identifiers
	 */
	public static Collection<String> getInstancesId(
			final Reservation reservation) {
		
		final List<Instance> instances = reservation.getInstances();
		final List<String> identifiers =  new ArrayList<String>(instances.size());
		for (Instance instance : instances) {
			identifiers.add(instance.getInstanceId());
		}
		return identifiers;
	}
	/**
	 * Return a list containing all the instances identifiers on the given reservation.
	 * @param reservation the AWS reservation
	 * @return a list of instance identifiers
	 */
	public static Collection<String> getInstancesId(
			final List<InstanceStateChange> instancesState) {
		
		final List<String> identifiers =  new ArrayList<String>(instancesState.size());
		for (InstanceStateChange instanceState : instancesState) {
			identifiers.add(instanceState.getInstanceId());
		}
		return identifiers;
	}
	/**
	 * Return a collection of all instance identifiers referring to a running or pending instance.
	 * @param instances a {@link List} of {@link String}
	 * @return a collection of running instances identifier.
	 */
	public static Collection<String> getInstancesIdForTerminate(
			final Set<org.codesketch.birch.common.Instance> instances) {
		final Set<String> identifiers = new HashSet<String>();
		
		for(final org.codesketch.birch.common.Instance instance : instances) {
			final InstanceStateName current = instance.state();
			if(InstanceStateName.Pending.equals(current) || InstanceStateName.Running.equals(current) ) {
				identifiers.add(instance.getInstanceId());
			}
		}
		return identifiers;
	}
	/**
	 * Check if the timeout has expired
	 * @param start the metho's execution start time
	 * @param timeout the expected timeout
	 * @return true if thte timeout has been exired false otherwise.
	 */
	public static Boolean timedout(
			final long start, final long timeout) {
		
		if(timeout<0) {
			throw new IllegalArgumentException(String.format("Timeout parameter cannot be < 0, %d given.", timeout));
		}
		boolean timedout = false;
		if(timeout>0) {
			long elapsed = System.currentTimeMillis() - start;
			System.out.println(elapsed);
			timedout = elapsed > timeout;
		}
		return timedout;
	}
}
